package org.text2diagram.ddl.util;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;

// http://www.gnu.org/software/bash/manual/bashref.html
// http://www.opennet.ru/docs/RUS/bash_scripting_guide/c12790.html (rus)
// http://www.cs.princeton.edu/~jlk/kornshell/doc/man93.html



public class ScriptExecutor {
	
	private static int NOT_COMPLETED = Integer.MAX_VALUE;
	
	private String[] command;
	private int maxLines;
	private String workingDir;
	private Map<String, String> environment;
	
	private LinkedBlockingQueue<String> outputTail;
	private String lastLine;
	
	private int resultCode = NOT_COMPLETED;
	
	public ScriptExecutor() {		
	}

	public ScriptExecutor(String... command) {
		super();
		this.command = command;
		this.maxLines = 20;		
	}


	public class ScriptOutputReader extends Thread {
		
		private InputStream inputStream;
		
		public ScriptOutputReader(InputStream inputStream) {
			outputTail = new LinkedBlockingQueue<String>(getMaxLines() - 1); //last line stored in lastLine
			lastLine = "";
			this.inputStream = inputStream;
			this.setDaemon(true);
			this.start();
		}
		
		@Override
		public void run() {
			while (true) {
				try {
					Thread.sleep(200);
					int availableBytes = inputStream.available();
					byte[] nextBlockArray = new byte[availableBytes];
					inputStream.read(nextBlockArray);
					
					String nextBlock = lastLine + new String(nextBlockArray, Charset.forName("IBM866"));
					String[] lines = nextBlock.split("\n");
					for (int i=0; i < lines.length - 2; i++) {
						boolean added = outputTail.offer(lines[i]);
						//if capacity reached remove first element and try again
						if (!added) {
							outputTail.remove();
							outputTail.offer(lines[i]);
						}
					}
					lastLine = lines[lines.length - 1];
				} catch (Exception e) {
					System.err.println("Exception reading input: \n" + e);
				}
			}
		}

	}
	
	@SuppressWarnings("serial")
	public static class ExecutionException extends RuntimeException {
		
		public ExecutionException(String message) {
			super(message);
		}

		public ExecutionException(Exception e) {
			super(e);
		}
		
	}
	
	public int getMaxLines() {
		return maxLines;
	}
	
	public String getOutputTail() {		
		String result = "";
		for (String line : outputTail)
			result += line + "\n";
		return result + lastLine;
	}
	
	private void executeCommand(ProcessBuilder processBuilder) {
		try {
			Process cmdProcess = processBuilder.start();			
			//execute in separate thread to be able to capture timeout

			//capture output
			new ScriptOutputReader(cmdProcess.getInputStream());
			resultCode = cmdProcess.waitFor();
		} catch (Exception e) {
			System.out.println();
			throw new ExecutionException(e);					
		}	
	}
	
	public void execute() {
		execute(0);
	}

	public void execute(long timeout) {
		try {
			final ProcessBuilder processBuilder = new ProcessBuilder(command);
			processBuilder.redirectErrorStream(true);
			if (environment != null)
				processBuilder.environment().putAll(environment);
			System.out.println(processBuilder.environment());
			if (workingDir != null)
				processBuilder.directory(new File(workingDir));

			if (timeout == 0) { //blocks until completed
				executeCommand(processBuilder);
			} else { //execute in a separate thread and wait for timeout then return with exception
				Thread executionThread = new Thread() {
					public void run() {
						executeCommand(processBuilder);
					}
				};
				executionThread.start();

				executionThread.join(timeout);
				if (resultCode == NOT_COMPLETED)
					throw new ExecutionException("Script did not complete in specified time frame of " + timeout + " ms");
			}
			Thread.sleep(500);//wait for output to be captured
		} catch (Exception e) {
			throw new ExecutionException(e);
		}
	}
	
	public void setEnvironment(Map<String, String> environment) {
		this.environment = environment;
	}

	public Map<String, String> getEnvironment() {
		return environment;
	}

	public void setWorkingDir(String workingDir) {
		this.workingDir = workingDir;
	}

	public String getWorkingDir() {
		return workingDir;
	}

	public void setMaxLines(int maxLines) {
		this.maxLines = maxLines;
	}

	public void setCommand(String... command) {
		this.command = command;
	}

	public String[] getCommand() {
		return command;
	}

	public void setResultCode(int resultCode) {
		this.resultCode = resultCode;
	}

	public int getResultCode() {
		return resultCode;
	}
	
	
	public static void main(String[] args) throws Exception {
//		ProcessBuilder processBuilder = new ProcessBuilder("cmd.exe", "/c", "echo", "%MYVAR%");

		ScriptExecutor executor = new ScriptExecutor();
		HashMap<String, String> env = new HashMap<String, String>();
		env.put("MYVAR", "myvalue");
		executor.setEnvironment(env);
		executor.setMaxLines(10);
		executor.setCommand("cmd.exe", "/?");
		
		executor.execute(5000);
		
		System.out.println("Script finished execution with result code: " + executor.getResultCode());
		System.out.println(executor.getOutputTail());
	}

}
